<?php
/** 
 * @brief 数据库驱动抽象类 
 */
abstract class Db_Adapter {
    /**
     * Keys are UPPERCASE SQL datatypes or the constants
     * Db::INT_TYPE, Db::BIGINT_TYPE, or Db::FLOAT_TYPE.
     *
     * Values are:
     * 0 = 32-bit integer
     * 1 = 64-bit integer
     * 2 = float or decimal
     *
     * @var array Associative array of datatypes to values 0, 1, or 2.
     */
    protected $_numericDataTypes = [
        Db::INT_TYPE    => Db::INT_TYPE,
        Db::BIGINT_TYPE => Db::BIGINT_TYPE,
        Db::FLOAT_TYPE  => Db::FLOAT_TYPE
    ];

    /** 
     * 数据库DSN配置
     *
     * @var array 
     */
    protected $_dsn;

    /** 
     * 数据库连接实例
     *
     * @var object 
     */
    protected $_conn;

    /** 
     * 表前缀名
     *
     * @var string 
     */
    public $tablePrefix = '';

    /** 
     * 上一次执行INSERT/UPDATE/DELETE查询所影响的记录行数
     *
     * @var int 
     */
    protected $_affectedRows = 0;

    /** 
     * 上一次执行SELECT查询所返回的记录行数
     *
     * @var int 
     */
    protected $_numRows = 0;

    /** 
     * 最终执行的SQL语句
     *
     * @var string 
     */
    protected $_lastSql;

    /**
     * 关闭数据库连接
     * 
     * @return void
     */
	abstract public function close();
	
    /**
     * 执行INSERT/UPDATE/DELETE查询并返回受影响的记录数
     * 
     * @param string $sql SQL语句
     * @param array $params 查询参数
     * 
     * @return int
     */ 
    abstract public function execute($sql, $params = []);

    /**
     * 执行SELECT查询并返回记录集对象 
     * 
     * @param string $sql SQL语句
     * @param array $params 查询参数
     * 
     * @return object
     */ 
    abstract public function query($sql, $params = []);
    
    /**
     * 取得上一次执行INSERT/UPDATE/DELETE查询所影响的记录行数
     * 
     * @return int
     */
    abstract public function affectedRows();

    /**
     * 取得上一次执行SELECT查询所返回的记录行数
     * 
     * @return int
     */
    abstract public function numRows();

    /**
     * 执行SELECT查询并返回某一页记录
     * 
     * @param string $sql SQL语句
     * @param array $params 查询参数
     * @param int $page 当前页码
     * @param int $pageSize 每页记录数
     * @param string $fetchMode 记录集的类型
     * 
     * @return mixed
     */ 
    abstract public function getPage($sql, $params, $page, $pageSize = 10, $fetchMode = Db::FETCH_ASSOC);

    /**
     * 执行SELECT查询并返回所有记录 
     * 
     * @param string $sql SQL语句
     * @param array $params 查询参数
     * @param string $fetchMode 记录集的类型
     * 
     * @return mixed
     */ 
    abstract public function getAll($sql, $params = [], $fetchMode = Db::FETCH_ASSOC);
    
    /**
     * 执行SELECT查询并返回第一条记录 
     * 
     * @param string $sql SQL语句
     * @param array $params 查询参数
     * @param string $fetchMode 记录集的类型
     * 
     * @return mixed
     */ 
    abstract public function getRow($sql, $params = [], $fetchMode = Db::FETCH_ASSOC);
    
    /**
     * 执行SELECT查询并返回第一条记录的第一个字段值
     * 
     * @param string $sql SQL语句
     * @param array $params 查询参数
     * 
     * @return mixed
     */ 
    abstract public function getOne($sql, $params = []);

    /**
     * 启动事务处理 
     * 
     * @return boolean
     */ 
    abstract public function startTrans();
    
    /**
     * 提交事务处理
     * 
     * @return boolean
     */ 
    abstract public function commitTrans();
    
    /**
     * 回滚事务处理 
     * 
     * @return boolean
     */ 
    abstract public function rollBack();

    /**
     * 生成最终执行的SQL语句 
     * 
     * @param string $sql SQL语句
     * @param array $params 查询参数
     * 
     * @return string
     */
    public function parseSql($sql, $params = []) {
        $count = count($params);
        if ($count > 0) {
            $sql = str_replace('?', '%s', $sql);
            $values = array();
            for ($i = 0; $i < $count; $i++) {
                if ($params[$i] == 'NOW()' || $params[$i] == 'now()') {
                    $values[$i] = $params[$i];
                } else {
                    $values[$i] = $this->quote($params[$i]);
                }
            }
            array_unshift($values, $sql);
            $this->_lastSql = call_user_func_array('sprintf', $values);
            $this->_lastSql = str_replace("'NULL'", 'NULL', $this->_lastSql);
        } else {
            $this->_lastSql = $sql;
        }
        return $this->_lastSql;
    }

    /**
     * 数据库操作错误提示信息
     *
     * @param string $message 错误信息
     * 
     * @return void
     */
    public function errorMsg($message) {
        Log::save($message . ': ' . $this->_lastSql, Log::SQL);
        throw new Exception($message);
    }

    /**
     * Quote a raw string.
     *
     * @param string $value     Raw string
     * @return string           Quoted string
     */
    protected function _quote($value) {
        if (is_int($value)) {
            return $value;
        } elseif (is_float($value)) {
            return "$value";
        }
        return "'" . addcslashes($value, "\000\n\r\\'\"\032") . "'";
    }

    /**
     * Creates and returns a new Db_Select object for this adapter.
     * 
     * @param string $cols Columns to select
     *
     * @return Db_Select
     */
    public function select($cols = '*') {
        return new Db_Select($this, $cols);
    }

    /**
     * Safely quotes a value for an SQL statement.
     *
     * If an array is passed as the value, the array values are quoted
     * and then returned as a comma-separated string.
     *
     * @param mixed $value The value to quote.
     * @param mixed $type  OPTIONAL the SQL datatype name, or constant, or null.
     * @return mixed An SQL-safe quoted value (or string of separated values).
     */
    public function quote($value, $type = null) {
        if ($value instanceof Db_Select) {
            return '(' . $value->assemble() . ')';
        }

        if ($value instanceof Db_Expr) {
            return $value->__toString();
        }

        if (is_array($value)) {
            foreach ($value as &$val) {
                $val = $this->quote($val, $type);
            }
            return implode(', ', $value);
        }

        if ($type !== null && array_key_exists($type = strtoupper($type), $this->_numericDataTypes)) {
            $quotedValue = '0';
            switch ($this->_numericDataTypes[$type]) {
                case Db::INT_TYPE: // 32-bit integer
                    $quotedValue = (string) intval($value);
                    break;
                case Db::BIGINT_TYPE: // 64-bit integer
                    // ANSI SQL-style hex literals (e.g. x'[\dA-F]+')
                    // are not supported here, because these are string
                    // literals, not numeric literals.
                    if (preg_match('/^(
                          [+-]?                  # optional sign
                          (?:
                            0[Xx][\da-fA-F]+     # ODBC-style hexadecimal
                            |\d+                 # decimal or octal, or MySQL ZEROFILL decimal
                            (?:[eE][+-]?\d+)?    # optional exponent on decimals or octals
                          )
                        )/x',
                        (string) $value, $matches)) {
                        $quotedValue = $matches[1];
                    }
                    break;
                case Db::FLOAT_TYPE: // float or decimal
                    //$quotedValue = sprintf('%F', $value);
                    $quotedValue = "$value";
            }
            return $quotedValue;
        }

        return $this->_quote($value);
    }

    /**
     * Quotes a value and places into a piece of text at a placeholder.
     *
     * The placeholder is a question-mark; all placeholders will be replaced
     * with the quoted value.   For example:
     *
     * <code>
     * $text = "WHERE date < ?";
     * $date = "2005-01-02";
     * $safe = $sql->quoteInto($text, $date);
     * // $safe = "WHERE date < '2005-01-02'"
     * </code>
     *
     * @param string  $text  The text with a placeholder.
     * @param mixed   $value The value to quote.
     * @param string  $type  OPTIONAL SQL datatype
     * @param integer $count OPTIONAL count of placeholders to replace
     * @return string An SQL-safe quoted value placed into the original text.
     */
    public function quoteInto($text, $value, $type = null, $count = null) {
        if ($count === null) {
            return str_replace('?', $this->quote($value, $type), $text);
        } else {
            while ($count > 0) {
                if (strpos($text, '?') !== false) {
                    $text = substr_replace($text, $this->quote($value, $type), strpos($text, '?'), 1);
                }
                --$count;
            }
            return $text;
        }
    }

    /**
     * Quotes an identifier.
     *
     * Accepts a string representing a qualified indentifier. For Example:
     * <code>
     * $adapter->quoteIdentifier('myschema.mytable')
     * </code>
     * Returns: "myschema"."mytable"
     *
     * Or, an array of one or more identifiers that may form a qualified identifier:
     * <code>
     * $adapter->quoteIdentifier(array('myschema','my.table'))
     * </code>
     * Returns: "myschema"."my.table"
     *
     * The actual quote character surrounding the identifiers may vary depending on
     * the adapter.
     *
     * @param string|array|Db_Expr $ident The identifier.
     * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
     * @return string The quoted identifier.
     */
    public function quoteIdentifier($ident, $auto = false) {
        return $this->_quoteIdentifierAs($ident, null, $auto);
    }

    /**
     * Quote a column identifier and alias.
     *
     * @param string|array|Db_Expr $ident The identifier or expression.
     * @param string $alias An alias for the column.
     * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
     * @return string The quoted identifier and alias.
     */
    public function quoteColumnAs($ident, $alias, $auto = false) {
        return $this->_quoteIdentifierAs($ident, $alias, $auto);
    }

    /**
     * Quote a table identifier and alias.
     *
     * @param string|array|Db_Expr $ident The identifier or expression.
     * @param string $alias An alias for the table.
     * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
     * @return string The quoted identifier and alias.
     */
    public function quoteTableAs($ident, $alias = null, $auto = false) {
        return $this->_quoteIdentifierAs($ident, $alias, $auto);
    }

    /**
     * Quote an identifier and an optional alias.
     *
     * @param string|array|Db_Expr $ident The identifier or expression.
     * @param string $alias An optional alias.
     * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
     * @param string $as The string to add between the identifier/expression and the alias.
     * @return string The quoted identifier and alias.
     */
    protected function _quoteIdentifierAs($ident, $alias = null, $auto = false, $as = ' AS ') {
        if ($ident instanceof Db_Expr) {
            $quoted = $ident->__toString();
        } elseif ($ident instanceof Db_Select) {
            $quoted = '(' . $ident->assemble() . ')';
        } else {
            if (is_string($ident)) {
                $ident = explode('.', $ident);
            }
            if (is_array($ident)) {
                $segments = array();
                foreach ($ident as $segment) {
                    if ($segment instanceof Db_Expr) {
                        $segments[] = $segment->__toString();
                    } else {
                        $segments[] = $this->_quoteIdentifier($segment, $auto);
                    }
                }
                if ($alias !== null && end($ident) == $alias) {
                    $alias = null;
                }
                $quoted = implode('.', $segments);
            } else {
                $quoted = $this->_quoteIdentifier($ident, $auto);
            }
        }
        if ($alias !== null) {
            $quoted .= $as . $this->_quoteIdentifier($alias, $auto);
        }
        return $quoted;
    }

    /**
     * Quote an identifier.
     *
     * @param  string $value The identifier or expression.
     * @param boolean $auto If true, heed the AUTO_QUOTE_IDENTIFIERS config option.
     * @return string        The quoted identifier and alias.
     */
    protected function _quoteIdentifier($value, $auto = false) {
        if ($auto === false) {
            $q = $this->getQuoteIdentifierSymbol();
            return ($q . str_replace("$q", "$q$q", $value) . $q);
        }
        return $value;
    }

    /**
     * Returns the symbol the adapter uses for delimited identifiers.
     *
     * @return string
     */
    public function getQuoteIdentifierSymbol() {
        return '"';
    }
}
